/*
 * Copyright 2014 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.aplink.generic.google.maps.heatmaps;

import java.util.HashMap;

import android.graphics.Color;

/**
 * A class to generate a color map from a given array of colors and the
 * fractions that the colors represent by interpolating between their HSV
 * values. This color map is to be used in the HeatmapTileProvider.
 */
public class Gradient {

	private class ColorInterval {
		private final int color1;
		private final int color2;

		/**
		 * The period over which the color changes from color1 to color2. This
		 * is given as the number of elements it represents in the colorMap.
		 */
		private final float duration;

		private ColorInterval(final int color1, final int color2,
				final float duration) {
			this.color1 = color1;
			this.color2 = color2;
			this.duration = duration;
		}
	}

	private static final int DEFAULT_COLOR_MAP_SIZE = 1000;

	/**
	 * Helper function for creation of color map Interpolates between two given
	 * colors using their HSV values.
	 *
	 * @param color1
	 *            First color
	 * @param color2
	 *            Second color
	 * @param ratio
	 *            Between 0 to 1. Fraction of the distance between color1 and
	 *            color2
	 * @return Color associated with x2
	 */
	static int interpolateColor(final int color1, final int color2,
			final float ratio) {

		final int alpha = (int) (((Color.alpha(color2) - Color.alpha(color1)) * ratio) + Color
				.alpha(color1));

		final float[] hsv1 = new float[3];
		Color.RGBToHSV(Color.red(color1), Color.green(color1),
				Color.blue(color1), hsv1);
		final float[] hsv2 = new float[3];
		Color.RGBToHSV(Color.red(color2), Color.green(color2),
				Color.blue(color2), hsv2);

		// adjust so that the shortest path on the color wheel will be taken
		if ((hsv1[0] - hsv2[0]) > 180) {
			hsv2[0] += 360;
		} else if ((hsv2[0] - hsv1[0]) > 180) {
			hsv1[0] += 360;
		}

		// Interpolate using calculated ratio
		final float[] result = new float[3];
		for (int i = 0; i < 3; i++) {
			result[i] = ((hsv2[i] - hsv1[i]) * (ratio)) + hsv1[i];
		}

		return Color.HSVToColor(alpha, result);
	}

	/**
	 * Size of a color map for the heatmap
	 */
	public final int mColorMapSize;

	/**
	 * The colors to be used in the gradient
	 */
	public int[] mColors;

	/**
	 * The starting point for each color, given as a percentage of the maximum
	 * intensity
	 */
	public float[] mStartPoints;

	/**
	 * Creates a Gradient with the given colors and starting points. These are
	 * given as parallel arrays.
	 *
	 * @param colors
	 *            The colors to be used in the gradient
	 * @param startPoints
	 *            The starting point for each color, given as a percentage of
	 *            the maximum intensity This is given as an array of floats with
	 *            values in the interval [0,1]
	 */
	public Gradient(final int[] colors, final float[] startPoints) {
		this(colors, startPoints, Gradient.DEFAULT_COLOR_MAP_SIZE);
	}

	/**
	 * Creates a Gradient with the given colors and starting points which
	 * creates a colorMap of given size. The colors and starting points are
	 * given as parallel arrays.
	 *
	 * @param colors
	 *            The colors to be used in the gradient
	 * @param startPoints
	 *            The starting point for each color, given as a percentage of
	 *            the maximum intensity This is given as an array of floats with
	 *            values in the interval [0,1]
	 * @param colorMapSize
	 *            The size of the colorMap to be generated by the Gradient
	 */
	public Gradient(final int[] colors, final float[] startPoints,
			final int colorMapSize) {
		if (colors.length != startPoints.length) {
			throw new IllegalArgumentException(
					"colors and startPoints should be same length");
		} else if (colors.length == 0) {
			throw new IllegalArgumentException("No colors have been defined");
		}
		for (int i = 1; i < startPoints.length; i++) {
			if (startPoints[i] <= startPoints[i - 1]) {
				throw new IllegalArgumentException(
						"startPoints should be in increasing order");
			}
		}
		mColorMapSize = colorMapSize;
		mColors = new int[colors.length];
		mStartPoints = new float[startPoints.length];
		System.arraycopy(colors, 0, mColors, 0, colors.length);
		System.arraycopy(startPoints, 0, mStartPoints, 0, startPoints.length);
	}

	private HashMap<Integer, ColorInterval> generateColorIntervals() {
		final HashMap<Integer, ColorInterval> colorIntervals = new HashMap<Integer, ColorInterval>();
		// Create first color if not already created
		// The initial color is transparent by default
		if (mStartPoints[0] != 0) {
			final int initialColor = Color.argb(0, Color.red(mColors[0]),
					Color.green(mColors[0]), Color.blue(mColors[0]));
			colorIntervals.put(0, new ColorInterval(initialColor, mColors[0],
					mColorMapSize * mStartPoints[0]));
		}
		// Generate color intervals
		for (int i = 1; i < mColors.length; i++) {
			colorIntervals
			.put(((int) (mColorMapSize * mStartPoints[i - 1])),
					new ColorInterval(
							mColors[i - 1],
							mColors[i],
							(mColorMapSize * (mStartPoints[i] - mStartPoints[i - 1]))));
		}
		// Extend to a final color
		// If color for 100% intensity is not given, the color of highest
		// intensity is used.
		if (mStartPoints[mStartPoints.length - 1] != 1) {
			final int i = mStartPoints.length - 1;
			colorIntervals.put(((int) (mColorMapSize * mStartPoints[i])),
					new ColorInterval(mColors[i], mColors[i], mColorMapSize
							* (1 - mStartPoints[i])));
		}
		return colorIntervals;
	}

	/**
	 * Generates the color map to use with a provided gradient.
	 *
	 * @param opacity
	 *            Overall opacity of entire image: every individual alpha value
	 *            will be multiplied by this opacity.
	 * @return the generated color map based on the gradient
	 */
	int[] generateColorMap(final double opacity) {
		final HashMap<Integer, ColorInterval> colorIntervals = generateColorIntervals();
		final int[] colorMap = new int[mColorMapSize];
		ColorInterval interval = colorIntervals.get(0);
		int start = 0;
		for (int i = 0; i < mColorMapSize; i++) {
			if (colorIntervals.containsKey(i)) {
				interval = colorIntervals.get(i);
				start = i;
			}
			final float ratio = (i - start) / interval.duration;
			colorMap[i] = Gradient.interpolateColor(interval.color1,
					interval.color2, ratio);
		}
		if (opacity != 1) {
			for (int i = 0; i < mColorMapSize; i++) {
				final int c = colorMap[i];
				colorMap[i] = Color.argb((int) (Color.alpha(c) * opacity),
						Color.red(c), Color.green(c), Color.blue(c));
			}
		}

		return colorMap;
	}

}
